package net.cyndeline.rldungeon.util

import scala.util.Random
import java.util
import scala.collection.JavaConversions._

/**
 * Maps weights towards objects, making it possible to increase the rate at which some objects are
 * returned compared to others.
 *
 * To make an item more likely to be returned, assign it a greater height.
 *
 * Source: http://stackoverflow.com/a/6409791
 *
 * @tparam E The type of object to assign weights to.
 */
class ProbabilityCollection[E](random: Random) extends RandomCollection[E] {
  private var weightAndElements: util.NavigableMap[Double, E] = new util.TreeMap() //TODO test that the same seeded random objects return the same data every time, so that NavigableMap guarantees the same result every time (kinda like how hashmaps shit up tests by being random)
  private var totalWeight: Double = 0

  /**
   * Constructs a new random collection from this collection.
   */
  private def this(random: Random, values: util.NavigableMap[Double, E]) = {
    this(random)
    val weightAndValues = values.iterator

    while (weightAndValues.hasNext) {
      val weightAndValue = weightAndValues.next()
      add(weightAndValue._1, weightAndValue._2)
    }
  }

  /**
   * Add an item and assign a weight to it.
   *
   * @param weight Weight to assign to the item. Must be higher than 0.
   * @param element Item to map height to.
   */
  override def add(weight: Double, element: E) {
    if (weight > 0) {
      totalWeight += weight
      weightAndElements.put(totalWeight, element)
    }
  }

  /**
   * Adds elements and weights of another collection to this one.
   * @param collection Collection to add.
   */
  override def addCollection(collection: RandomCollection[E]) {
    val elements = collection.iterator
    while (elements.hasNext) {
      val e: (Double, E) = elements.next()
      add(e._1, e._2)
    }
  }

  /**
   * Removes an element from the collection by creating a new map from the set of old values
   * minus the one to be removed.
   * @param element Element to remove.
   */
  override def remove(element: E) {
    val elements = weightAndElements.iterator
    weightAndElements = new util.TreeMap()

    while (elements.hasNext) {
      val e: (Double, E) = elements.next()
      if (e._2 != element)
        add(e._1, e._2)
    }
  }

  /**
   * Fetches an item from the collection at random. items with higher
   * weight will be more likely to be returned.
   *
   * @return A random item with weighted probability that skews towards
   * 			greater weights.
   */
  override def next: E = {
    val value = random.nextDouble * totalWeight
    weightAndElements.ceilingEntry(value).getValue
  }

  /**
   * @return the size of the collection.
   */
  override  def size: Int = weightAndElements.size

  /**
   * @return True if the collection contains no elements, otherwise false.
   */
  override def isEmpty: Boolean = size == 0

  /**
   * Returns each element stored without weights.
   * @return A list containing one copy of each element in no
   * 			particular order.
   */
  override def allElements: List[E] = weightAndElements.map(weightAndElement => weightAndElement._2).toList

  /**
   * Iteratoes over every weight and element in the collection.
   * @return An iterator over the collection.
   */
  override def iterator: Iterator[(Double, E)] = weightAndElements.iterator

  /**
   * Creates a copy of this collection using the same Random object.
   * @return A new collection with the same weights and elements.
   */
  override def copy: RandomCollection[E] = newCollection(random)

  /**
   * Builds a collection using a new random object and the same values as the old collection.
   * @param random Random object used when selecting elements.
   * @return A RandomCollection with a new Random object and the same elements and weights as the old collection.
   */
  override def newCollection(random: Random): RandomCollection[E] = new ProbabilityCollection[E](random, weightAndElements)
}
